const express = require('express')
const logger = require('morgan')
const { StatusCodes } = require('http-status-codes')
const cors = require('cors')
const fs = require('fs-extra')
const path = require('path')
const CHUNK_SIZE = 100 * 1024 * 1024

const PUBLIC_DIR = path.resolve(__dirname, 'public')
const TEMP_DIR = path.resolve(__dirname, 'temp')
// 存放上传并合并好的文件
fs.ensureDirSync(PUBLIC_DIR)
// 存放分片的文件
fs.ensureDirSync(TEMP_DIR)
const app = express()
app.use(logger('dev'))
app.use(cors())
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use(express.static(PUBLIC_DIR))

/**
 * 上传分片
 */
app.post(`/upload/:fileName`, async (req, res, next) => {
  // 通过路径参数获取文件名
  const { fileName } = req.params
  // 通过查询参数获取文件名
  const { chunkFileName } = req.query
  // 写入文件的起始位置
  const start = isNaN(req.query.start) ? 0 : parseInt(req.query.start, 10)
  // 创建用户保存此文件的分片的目录
  const chunkDir = path.resolve(TEMP_DIR, fileName)
  // 分片的路径
  const chunkFilePath = path.resolve(chunkDir, chunkFileName)
  // 先确定分片目录存在
  await fs.ensureDirSync(chunkDir)
  // 创建此文件的可写流，可以指定写入的起始位置
  const ws = fs.createWriteStream(chunkFilePath, { start, flags: 'a' }) // flags: a 以追加模式打开文件，如果文件不存在则创建文件
  // 后面会实现暂停操作，如果客户端点击了暂停按钮，会取消上传的操作，取消之后会在服务器触发请求aborted事件，关闭可写流
  req.on('aborted', () => { ws.close() })
  // 使用管道的方式把请求中的请求体流数据写入到文件中
  try {
    await pipeStream(req, ws)
    res.json({ success: true })
  } catch (error) {
    next(error)
  }
})

app.get(`/merge/:fileName`, async (req, res, next) => {
  // 通过路径参数获取文件名
  const { fileName } = req.params
  console.log('merge fileName', fileName);
  try {
    await mergeChunks(fileName)
    res.json({ success: true })
  } catch (error) {
    next(error)
  }
})
app.get('/verify/:fileName', async (req, res, next) => {
  const { fileName } = req.params
  // 先获取文件在服务器的路径
  const filePath = path.resolve(PUBLIC_DIR, fileName)
  // 判断是文件在服务器端是否存在
  const isExist = await fs.pathExists(filePath)
  // 如果已经存在了，则直接返回不需要上传了
  if (isExist) {
    return res.json({ success: true, needUpload: false })
  }
  const chunksDir = path.resolve(TEMP_DIR, fileName)
  const existDir = await fs.pathExists(chunksDir)
  // 存放已经上传的分片的对象数组
  let uploadedChunkList = []
  if (existDir) {
    // 读取临时目录里面的所有分片对应的文件
    const chunkFileNames = await fs.readdir(chunksDir)
    // 读取每个分片文件信息，主要是它的文件大小，表示已经上传的文件大小
    uploadedChunkList = await Promise.all(chunkFileNames.map(async function (chunkFileName) {
      const { size } = await fs.stat(path.resolve(chunksDir, chunkFileName))
      return { chunkFileName, size }
    }))
  }
  // 如果没有此文件，则意味着服务器还需要你上传此文件
  // 返回上传未完成，但是已经上传了一部分了，就把已经上传的分片名，以及分片的大小给客户端
  // 客户端就可以只上传分片剩下的数据部分了
  res.json({ success: true, needUpload: true, uploadedChunkList })
})
async function mergeChunks(fileName) {
  const mergedFilePath = path.resolve(PUBLIC_DIR, fileName)
  const chunkDir = path.resolve(TEMP_DIR, fileName)
  const chunkFiles = await fs.readdir(chunkDir)
  // 对分片按索引进行升序排序
  chunkFiles.sort((a, b) => Number(a.split('-')[1]) - Number(b.split('-')[1]))
  try {
    // 为了提高性能，我们在这时可以分片并行写入
    const pipes = chunkFiles.map((chunkFile, index) => {
      return pipeStream(
        fs.createReadStream(path.resolve(chunkDir, chunkFile), { autoClose: true }),
        fs.createWriteStream(mergedFilePath, { start: index * CHUNK_SIZE })
      )
    })
    // 并发把每个分片的数据写入到目标文件中
    await Promise.all(pipes)
    // 删除分片的文件和文件夹
    await fs.rmdir(chunkDir, { recursive: true })
    // 合并完文件之后可以重新在这里计算合并后文件的hash值，和文件中的hash值进行对比
    // 如果值是一样的，说明肯定内容是对的，没有被修改
  } catch (error) {
    next(error)
  }
}

function pipeStream(rs, ws) {
  return new Promise((resolve, reject) => {
    // 把可读流中的数据写入可写流中
    rs.pipe(ws).on('finish', resolve).on('error', reject)
  })
}

app.listen(8080, () => console.log('Server started on port 8080'))
